package nl.zoidberg.calculon.engine;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import nl.zoidberg.calculon.analyzer.GameScorer;
import nl.zoidberg.calculon.model.Board;
import nl.zoidberg.calculon.notation.PGNUtils;
import nl.zoidberg.calculon.opening.OpeningBook;

public class SearchNode {
	private static final int DEPTH = 3;
	private static int MAX_CACHE = Integer.parseInt(System.getProperty("calculon.maxcache", "250000"));
	private static final Logger log = Logger.getLogger(SearchNode.class.getName());
	
	public static long mSearch = 0, mHit = 0;
	
	private Board board;
	private Map<String, SearchNode> childNodes;
	
	public float getAlphaBetaScore(int depth, GameScorer scorer, Map<String, Float> cache) {
		return alphaBetaMax(Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, depth, scorer, cache);
	}
	
	private float alphaBetaMax(float alpha, float beta, int depthLeft, GameScorer scorer, Map<String, Float> cache) {
		if(depthLeft == 0 || getChildNodes().size() == 0) {
			mSearch++;
			String id = board.getCacheId();
			Float f = cache.get(id);
			if(f != null) {
				mHit++;
				return f.floatValue();
			}
			float rv = scorer.score(board);
			if(cache.size() < MAX_CACHE) {
				cache.put(id, rv);
			}
			return rv;
		}
		
		for(String key: getChildNodes().keySet()) {
			SearchNode childNode = getChildNodes().get(key);
			float score = childNode.alphaBetaMin(alpha, beta, depthLeft - 1, scorer, cache);
			childNode.reset();
			if(score >= beta) {
				return beta;
			}
			if(score > alpha) {
				alpha = score;
			}
		}
		
		return alpha;
	}

	private float alphaBetaMin(float alpha, float beta, int depthLeft, GameScorer scorer, Map<String, Float> cache) {
		if(depthLeft == 0 || getChildNodes().size() == 0) {
			mSearch++;
			String id = board.getCacheId();
			Float f = cache.get(id);
			if(f != null) {
				mHit++;
				return f.floatValue() * -1;
			}
			float rv = scorer.score(board);
			if(cache.size() < MAX_CACHE) {
				cache.put(id, rv);
			}
			return rv * -1;
		}

		for(String key: getChildNodes().keySet()) {
			SearchNode childNode = getChildNodes().get(key);
			float score = childNode.alphaBetaMax(alpha, beta, depthLeft - 1, scorer, cache);
			childNode.reset();
			if(score <= alpha) {
				return alpha;
			}
			if(score < beta) {
				beta = score;
			}
		}
		
		return beta;
	}
	
	public void reset() {
		childNodes = null;
	}
	
	public SearchNode(Board board) {
		this.board = board;
	}

	public String toString() {
		return "SearchNode[" + board + "]";
	}

	public Map<String, SearchNode> getChildNodes() {
		if(childNodes == null) {
				childNodes = MoveGenerator.get().generateMoves(board);
		}
		return childNodes;
	}

	public Board getBoard() {
		return board;
	}
	
	public Map<String, Float> getScoredMoves(GameScorer scorer) {
		Map<String, Float> rv = new HashMap<String, Float>();
		Map<String, Float> cache = new HashMap<String, Float>(MAX_CACHE);
		for (String move : this.getChildNodes().keySet()) {
			float score = this.getChildNodes().get(move).getAlphaBetaScore(DEPTH, scorer, cache);
			rv.put(move, score);
		}
		return rv;
	}
	
	public String getBestMove(GameScorer scorer) {
		return selectBestMove(getScoredMoves(scorer));
	}

	public static String selectBestMove(Map<String, Float> allMoves) {
		Map<String, Float> bestMoves = selectBestMoves(allMoves);
		if(bestMoves.size() == 0) {
			return null;
		}
		List<String> moves = new ArrayList<String>(bestMoves.keySet());
		return moves.get((int) (Math.random() * moves.size()));
	}
	
	public String getPreferredMove(GameScorer scorer) {
		Map<String, Float> allMoves = SearchNode.selectBestMoves(getScoredMoves(scorer));
		if(allMoves.size() > 1) {
			allMoves = rescoreImmediate(allMoves, scorer);
		}
		String rv = SearchNode.selectBestMove(allMoves);
		return rv;
	}
	
	public String getPreferredMove() {
		String bookMove = OpeningBook.getDefaultBook() == null ? null : OpeningBook.getDefaultBook().getBookMove(board); 
		if(bookMove != null) {
			log.finer("Using book move: " + bookMove);
			return PGNUtils.toPgnMoveMap(board).get(bookMove);
		}
		return getPreferredMove(GameScorer.getDefaultScorer());
	}
	
	public static Map<String, Float> selectBestMoves(Map<String, Float> allMoves) {
		float bestScore = Float.POSITIVE_INFINITY;
		Map<String, Float> bestMoves = new HashMap<String, Float>();
		for (String move : allMoves.keySet()) {
			float score = allMoves.get(move);
			if (score < bestScore) {
				bestMoves.clear();
				bestMoves.put(move, score);
				bestScore = score;
			} else if (score == bestScore) {
				bestMoves.put(move, score);
			}
		}
		
		return bestMoves;
	}
	
	public Map<String, Float> rescoreImmediate(Map<String, Float> allMoves, GameScorer scorer) {
		Map<String, Float> cache = new HashMap<String, Float>(1000);
		Map<String, Float> rv = new HashMap<String, Float>();
		for (String move : allMoves.keySet()) {
			float score = this.getChildNodes().get(move).getAlphaBetaScore(1, scorer, cache);
			rv.put(move, score);
		}
		return rv;
	}
}
